最近蘋果出了 Apple Watch Series 8,看了我牙癢癢,於是咬緊牙關,把他的錶面臨摹出來了(?)用 Canvas 畫時鐘的教學有很多,今天也參考了很多前輩大大還有 Mdn 的文件教學,當然如果大家有覺得怎麼寫可以更好~歡迎底下留言跟我分享。這個錶面就是我們今天臨摹的模特兒,看仔細了嗎?那我們就開始吧!
在做動畫的時後我們可以先把要畫出來的圖像分為「靜止」與「動態」的兩種。靜止的例如時鐘的圓底、刻度、數字等,會動的則有時針、分針、秒針。靜止的圖像通常比較簡單一點,這次我們先依序完成錶面、刻度與數字。刻度的部分是經由畫線、旋轉並透過 for 迴圈完成重複的繪圖。
// 1 分刻度
ctx.save();
ctx.lineWidth = 3;
for (let i = 0; i < 60; i++) {
if (i % 5 !== 0) {
ctx.beginPath();
ctx.moveTo(117, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.rotate(Math.PI / 30);
}
ctx.restore();
// 5 分刻度
ctx.save();
for (let i = 0; i < 12; i++) {
ctx.beginPath();
ctx.rotate(Math.PI / 6);
ctx.moveTo(115, 0);
ctx.lineTo(120, 0);
ctx.stroke();
}
ctx.restore();
嘿嘿嘿該來的還是來了,繪製小時時竟然出現了 cos 和 sin!我們例如三角函式來計算每一個數字的坐標位置,這個公式可以拆解成先將角度轉換成弧度,再用三角函式來算出x, y 距離,這裡的 θ 是 30 度也是每一個數字之間間隔的角度。
// 小時數字
const hourNum = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1];
ctx.save();
ctx.beginPath();
ctx.font = "25px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
ctx.rotate(Math.PI / 2);
for (let i = 0; i < hourNum.length; i++) {
let x = 95 * Math.cos(((i * 30 - 60) * Math.PI) / 180);
let y = 95 * Math.sin(((i * 30 - 60) * Math.PI) / 180);
ctx.fillText(hourNum[i], x, y);
}
}
ctx.restore();
剩下的會動的部分主要可以拆解為秒針、分針與時針,他們的差異主要就是「選轉角度變化」,我們可以使用 rotate()
的 API 去控制旋轉的速率。這邊分針和秒針為例,我們可以把旋轉角度可以拆成 60 步。
秒針最好理解是每秒轉一格,ctx.rotate((sec * Math.PI) / 30)
的 30 就代表了 1/60 個圓。而分針是每 60 秒轉一格,我們每次抓的旋轉角度是「整數的分鐘」加上「不到一分鐘的秒數」,也就是下方 ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec)
的由來。
如果忘記 Rotate 後面的角度設定方法可以回去看看這篇文章唷: 第 6 幅 - 圖形應用:旋轉!變形!我閉著眼
const sec = now.getSeconds();
const min = now.getMinutes();
const hr = now.getHours() % 12;
// 分針
ctx.save();
ctx.rotate((Math.PI / 30) * min + (Math.PI / 1800) * sec);
ctx.lineWidth = 14;
ctx.beginPath();
ctx.moveTo(30, 0);
ctx.lineTo(112, 0);
ctx.strokeStyle = "black";
ctx.stroke();
ctx.restore();
ctx.beginPath();
ctx.arc(0, 0, 10, 0, Math.PI * 2, true);
ctx.fill();
// 秒針
ctx.save();
ctx.rotate((sec * Math.PI) / 30);
ctx.strokeStyle = "#E41515";
ctx.fillStyle = "white";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-30, 0);
ctx.lineTo(105, 0);
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = "black";
ctx.arc(0, 0, 5, 0, Math.PI * 2, true);
ctx.fill();
ctx.restore();
最後,當所有動畫都繪製完成後,我們可以透過 requestAnimationFrame()
這個 API 來呼叫動畫函式。requestAnimationFrame()
主要的功能為
向瀏覽器請求在下次重繪前呼叫這個動畫函式。callback 的次數通常落在每秒 60 次,當頁面處於背景或隱藏狀態時,多數的瀏覽器會暫停
requestAnimationFrame()
的呼叫。—— mdn
所以我們就在繪製的函式最後一行透過 requestAnimationFrame()
呼叫自己,這樣就可以完成我們的動畫更新機制。
// 動畫函式
const appleWatch = () => {
// 下次重繪前呼叫 appleWatch 動畫函式
window.requestAnimationFrame(appleWatch)
}
// 首次渲染
window.requestAnimationFrame(appleWatch)
恭喜你已經完成了時鐘,做出來之後這個 Canvas Tag 可以放在網頁專案的任何一處,讓你的網頁有獨一無二的時鐘唷,當然也可以進階應用在番茄鐘、碼表倒數計時等專案中,非常期待看到大家自己的創意運用。如果你喜歡這個練習,歡迎抖內我 Apple Watch 我是說歡迎留言跟我互動,我們明天見!
觀念參考文章:
https://www.796t.com/content/1546830379.html
https://ithelp.ithome.com.tw/articles/10198918
https://creativecoding.in/2021/05/14/來用可怕的三角函數做網頁吧-part2-科幻時鐘/
實作參考 mdn:https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Basic_animations
https://developer.mozilla.org/zh-TW/docs/Web/API/window/requestAnimationFrame
做時鐘做到快午夜豪可怕!嗚嗚希望我的一天有多一點時間QQ